/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package toolchain

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"
	"time"

	"mynewt.apache.org/newt/util"
)

type DepTracker struct {
	// Most recent .o modification time.
	MostRecentName string
	MostRecentTime time.Time

	compiler *Compiler
}

func NewDepTracker(c *Compiler) DepTracker {
	tracker := DepTracker{
		MostRecentName: "???",
		MostRecentTime: time.Unix(0, 0),
		compiler:       c,
	}

	return tracker
}

func (d *DepTracker) SetMostRecent(name string, t time.Time) {
	d.MostRecentName = name

	// Truncate sub-second part of timestamp.  Timestamps of generated files
	// seems to differ among tools.  See
	// https://github.com/apache/mynewt-newt/pull/276 for details.
	d.MostRecentTime = time.Unix(t.Unix(), 0)
}

// @return string               The name of the dependent file (i.e., the first
//                                  .o file encountered).
// @return []string             Populated with the dependencies' filenames.
func parseDepsLine(line string) (string, []string, error) {
	tokens := strings.Fields(line)
	if len(tokens) == 0 {
		return "", nil, nil
	}

	dFileTok := tokens[0]
	if dFileTok[len(dFileTok)-1:] != ":" {
		return "", nil, util.NewNewtError("line missing ':'")
	}

	dFileName := dFileTok[:len(dFileTok)-1]
	return dFileName, tokens[1:], nil

}

// Parses a dependency (.d) file generated by gcc.  On success, the returned
// string array is populated with the dependency filenames.  This function
// expects each line of a dependency file to have the following format:
//
// <file>.o: <file>.c a.h b.h c.h \
//  d.h e.h f.h
//
// Only the first dependent object(<file>.o) is considered.
//
// @return []string             Populated with the dependencies' filenames.
func ParseDepsFile(filename string) ([]string, error) {
	lines, err := util.ReadLines(filename)
	if err != nil {
		return nil, err
	}

	if len(lines) == 0 {
		return []string{}, nil
	}

	var dFile string
	allDeps := []string{}
	for _, line := range lines {
		src, deps, err := parseDepsLine(line)
		if err != nil {
			return nil, util.FmtNewtError(
				"Invalid Makefile dependency file \"%s\"; %s",
				filename, err.Error())
		}

		if dFile == "" {
			dFile = src
		}

		if src == dFile {
			allDeps = append(allDeps, deps...)
		}
	}

	return allDeps, nil
}

// Updates the dependency tracker's most recent timestamp according to the
// modification time of the specified file.  If the specified file is older
// than the tracker's currently most-recent time, this function has no effect.
func (tracker *DepTracker) ProcessFileTime(file string) error {
	modTime, err := util.FileModificationTime(file)
	if err != nil {
		return err
	}

	if modTime.After(tracker.MostRecentTime) {
		tracker.SetMostRecent(file, modTime)
	}

	return nil
}

// Determines if a file was previously built with a command line invocation
// different from the one specified.
//
// @param dstFile               The output file whose build invocation is being
//                                  tested.
// @param cmd                   The command that would be used to generate the
//                                  specified destination file.
//
// @return                      true if the command has changed or if the
//                                  destination file was never built;
//                              false otherwise.
func commandHasChanged(dstFile string, cmd []string) bool {
	cmdFile := dstFile + ".cmd"
	prevCmd, err := ioutil.ReadFile(cmdFile)
	if err != nil {
		return true
	}

	curCmd := serializeCommand(cmd)

	changed := bytes.Compare(prevCmd, curCmd) != 0
	return changed
}

func logRebuildReqd(dest string, reason string) {
	util.StatusMessage(util.VERBOSITY_VERBOSE,
		"%s - rebuild required; %s\n", dest, reason)
}

func logRebuildReqdCmdChanged(dest string) {
	logRebuildReqd(dest, "different command")
}

func logRebuildReqdModTime(dest string, src string) {
	logRebuildReqd(dest, fmt.Sprintf(
		"source (%s) newer than destination", src))
}

func logRebuildReqdNoDep(dest string, dep string) {
	logRebuildReqd(dest, fmt.Sprintf(
		"dependency \"%s\" has been deleted", dep))
}

func logRebuildReqdNewDep(dest string, dep string) {
	logRebuildReqd(dest, fmt.Sprintf(
		"destination older than dependency (%s)", dep))
}

// Determines if the specified C or assembly file needs to be built.  A compile
// is required if any of the following is true:
//     * The destination object file does not exist.
//     * The existing object file was built with a different compiler
//       invocation.
//     * The source file has a newer modification time than the object file.
//     * One or more included header files has a newer modification time than
//       the object file.
func (tracker *DepTracker) CompileRequired(srcFile string,
	compilerType int) (bool, error) {

	objPath := tracker.compiler.dstFilePath(srcFile) + ".o"
	depPath := tracker.compiler.dstFilePath(srcFile) + ".d"

	// If the object was previously built with a different set of options, a
	// rebuild is necessary.
	cmd, err := tracker.compiler.CompileFileCmd(srcFile, compilerType)
	if err != nil {
		return false, err
	}

	if commandHasChanged(objPath, cmd) {
		logRebuildReqdCmdChanged(srcFile)
		err := tracker.compiler.GenDepsForFile(srcFile)
		if err != nil {
			return false, err
		}
		return true, nil
	}

	if util.NodeNotExist(depPath) {
		err := tracker.compiler.GenDepsForFile(srcFile)
		if err != nil {
			return false, err
		}
	}

	srcModTime, err := util.FileModificationTime(srcFile)
	if err != nil {
		return false, err
	}

	objModTime, err := util.FileModificationTime(objPath)
	if err != nil {
		return false, err
	}

	// If the object doesn't exist or is older than the source file, a build is
	// required; no need to check dependencies.
	if srcModTime.After(objModTime) {
		logRebuildReqdModTime(objPath, srcFile)
		return true, nil
	}

	// Determine if the dependency (.d) file needs to be generated.  If it
	// doesn't exist or is older than the source file, it is out of date and
	// needs to be created.
	depModTime, err := util.FileModificationTime(depPath)
	if err != nil {
		return false, err
	}

	if srcModTime.After(depModTime) {
		err := tracker.compiler.GenDepsForFile(srcFile)
		if err != nil {
			return false, err
		}
	}

	// Extract the dependency filenames from the dependency file.
	deps, err := ParseDepsFile(depPath)
	if err != nil {
		return false, err
	}

	// Check if any dependencies are newer than the destination object file.
	for _, dep := range deps {
		if util.NodeNotExist(dep) {
			// The dependency has been deleted; a rebuild is required.  Also,
			// the dependency file is out of date, so it needs to be deleted.
			// We cannot regenerate it now because the source file might be
			// including a nonexistent header.
			logRebuildReqdNoDep(srcFile, dep)
			os.Remove(depPath)
			return true, nil
		} else {
			depModTime, err = util.FileModificationTime(dep)
			if err != nil {
				return false, err
			}
		}

		if depModTime.After(objModTime) {
			logRebuildReqdNewDep(srcFile, dep)
			return true, nil
		}
	}

	return false, nil
}

// Determines if the specified static library needs to be rearchived.  The
// library needs to be archived if any of the following is true:
//     * The destination library file does not exist.
//     * The existing library file was built with a different compiler
//       invocation.
//     * One or more source object files has a newer modification time than the
//       library file.
func (tracker *DepTracker) ArchiveRequired(archiveFile string,
	objFiles []string) (bool, error) {

	// If the archive was previously built with a different set of options, a
	// rebuild is required.
	cmd := tracker.compiler.CompileArchiveCmd(archiveFile, objFiles)
	if commandHasChanged(archiveFile, cmd) {
		logRebuildReqdCmdChanged(archiveFile)
		return true, nil
	}

	// If the archive doesn't exist or is older than any object file, a rebuild
	// is required.
	aModTime, err := util.FileModificationTime(archiveFile)
	if err != nil {
		return false, err
	}
	if tracker.MostRecentTime.After(aModTime) {
		logRebuildReqdModTime(archiveFile, tracker.MostRecentName)
		return true, nil
	}

	// The library is up to date.
	return false, nil
}

// Determines if the specified elf file needs to be linked.  Linking is
// necessary if the elf file does not exist or has an older modification time
// than any source object or library file.
// Determines if the specified static library needs to be rearchived.  The
// library needs to be archived if any of the following is true:
//     * The destination library file does not exist.
//     * The existing library file was built with a different compiler
//       invocation.
//     * One or more source object files has a newer modification time than the
//       library file.
func (tracker *DepTracker) LinkRequired(dstFile string,
	options map[string]bool, objFiles []string,
	keepSymbols []string, elfLib string) (bool, error) {

	// If the elf file was previously built with a different set of options, a
	// rebuild is required.
	cmd := tracker.compiler.CompileBinaryCmd(dstFile, options, objFiles, keepSymbols, elfLib)
	if commandHasChanged(dstFile, cmd) {
		logRebuildReqdCmdChanged(dstFile)
		return true, nil
	}

	// If the elf file doesn't exist or is older than any input file, a rebuild
	// is required.
	dstModTime, err := util.FileModificationTime(dstFile)
	if err != nil {
		return false, err
	}

	// If the elf file doesn't exist or is older than any input file, a rebuild
	// is required.
	if elfLib != "" {
		elfDstModTime, err := util.FileModificationTime(elfLib)
		if err != nil {
			return false, err
		}
		if elfDstModTime.After(dstModTime) {
			logRebuildReqdModTime(dstFile, elfLib)
			return true, nil
		}
	}

	// Check timestamp of each .o file in the project.
	if tracker.MostRecentTime.After(dstModTime) {
		logRebuildReqdModTime(dstFile, tracker.MostRecentName)
		return true, nil
	}

	// Check timestamp of the linker script and all input libraries.
	for _, ls := range tracker.compiler.LinkerScripts {
		objFiles = append(objFiles, ls)
	}
	for _, obj := range objFiles {
		objModTime, err := util.FileModificationTime(obj)
		if err != nil {
			return false, err
		}

		if objModTime.After(dstModTime) {
			logRebuildReqdNewDep(dstFile, obj)
			return true, nil
		}
	}

	return false, nil
}

/* Building a ROM elf is used for shared application linking.
 * A ROM elf requires a rebuild if any of archives (.a files) are newer
 * than the rom elf, or if the elf file is newer than the rom_elf */
func (tracker *DepTracker) RomElfBuildRequired(dstFile string, elfFile string,
	archFiles []string) (bool, error) {

	// If the rom_elf file doesn't exist or is older than any input file, a
	// rebuild is required.
	dstModTime, err := util.FileModificationTime(dstFile)
	if err != nil {
		return false, err
	}

	// If the elf file doesn't exist or is older than any input file, a rebuild
	// is required.
	elfDstModTime, err := util.FileModificationTime(elfFile)
	if err != nil {
		return false, err
	}

	if elfDstModTime.After(dstModTime) {
		logRebuildReqdModTime(dstFile, elfFile)
		return true, nil
	}

	for _, arch := range archFiles {
		objModTime, err := util.FileModificationTime(arch)
		if err != nil {
			return false, err
		}

		if objModTime.After(dstModTime) {
			logRebuildReqdModTime(dstFile, arch)
			return true, nil
		}
	}
	return false, nil
}

// Determines if the specified static library needs to be copied.  The
// library needs to be archived if any of the following is true:
//     * The destination library file does not exist.
//     * Source object files has a newer modification time than the
//       target file.
func (tracker *DepTracker) CopyRequired(srcFile string) (bool, error) {

	tgtFile := tracker.compiler.DstDir() + "/" + filepath.Base(srcFile)

	// If the target doesn't exist or is older than source file, a copy
	// is required.
	srcModTime, err := util.FileModificationTime(srcFile)
	if err != nil {
		return false, err
	}
	tgtModTime, err := util.FileModificationTime(tgtFile)
	if err != nil {
		return false, err
	}
	if srcModTime.After(tgtModTime) {
		return true, nil
	}

	// The target is up to date.
	return false, nil
}
